在現代框架的單頁應用 (Single Page Application, SPA) 中,其中一個技術是在不重整頁面的情況下,讓使用者感受到順暢的頁面過渡效果,這個技術的背後就是 History API,今天要跟大家分享 History API 究竟幫我們做了哪些事情?他在 SPA 中又扮演了什麼角色?
History API 可以讓開發者操作瀏覽器的歷史記錄。我們可以在不重整頁面的情況下更新 URL,還能讓瀏覽器的上一頁、下一頁正常運作。
History API 的特性包括:
這些特性,其實就跟傳統的多頁面網站一樣,也就是說,透過使用 History API,我們可以建立像傳統多頁面網站的單頁應用,同時又保持單頁應用的速度和流暢性。
History API 提供了幾個方法,讓開發者可以操作歷史紀錄
這個方法讓我們向瀏覽歷史中增加一個新的狀態。它的語法如下:
history.pushState(state, title, url)
state
: 一個 JavaScript 物件,包含與新歷史記錄相關的數據。title
:新頁面的標題。url
:新頁面的網址。這個方法與 pushState()
類似,但他會替換當前的歷史記錄,而不是增加新的紀錄。
history.replaceState(state, title, url)
他會在使用者使用瀏覽器上一頁、下一頁功能時觸發,我們可以使用它來處理狀態的變化。
window.onpopstate = function(event) {
// 處理狀態變化
console.log(event.state);
}
我放了三個連結,以及一個 id=content
的元素顯示對應頁面的內容
<nav>
<a href="home">首頁</a>
<a href="about">關於</a>
<a href="contact">聯絡我們</a>
</nav>
<div id="content"></div>
我們希望點不同連結,會顯示對應的內容,將這個動作包成一個函式 updateContent()
,根據 page
參數顯示資料
function updateContent(page) {
document.getElementById('content').innerHTML = `這是 ${page} 頁面的內容`;
}
呼叫 updateContent()
的時機點:
load()
window.onpopstate
navigateTo()
function navigateTo(page) {
const state = { page: page };
const title = page;
const url = `./${page}`;
history.pushState(state, title, url);
updateContent(page);
}
window.onpopstate = function (event) {
if (event.state) {
updateContent(event.state.page);
}
};
window.addEventListener('load', function () {
const initialPage = window.location.pathname || 'home';
updateContent(initialPage);
});
用 querySelectorAll
取得所有 nav a
元素,並呼叫 navigateTo()
函式。而要實現不重整頁面的效果,記得用 e.preventDefault()
阻擋頁面預設的行為
document.querySelectorAll('nav a').forEach(link => {
link.addEventListener('click', function (e) {
// 阻擋頁面預設行為,防止頁面跳轉
e.preventDefault();
const page = this.getAttribute('href');
navigateTo(page);
});
});
navigateTo()
有用到 history.pushState()
,我們在點擊連結時,會增加歷史瀏覽的紀錄,能有效切換上下頁。
function navigateTo(page) {
const state = { page: page };
const title = page;
const url = `./${page}`;
// 新增一筆歷史紀錄
history.pushState(state, title, url);
updateContent(page);
}
讓我們再更深入的做一些好玩的應用!
跟傳統的多頁面網站不同,SPA 的網址處理機制需要特別的設計。在傳統網站中,每個 URL 都對應一個實際頁面。然而,在 SPA 中,所有的內容都在一個頁面中動態載入,因此我們要手動處理 URL 的變化。
當使用者在瀏覽器中輸入一個 URL 時,我們需要確保應用能夠正確地載入對應的內容。這就是深度連結的核心概念。
前端需要監聽 load()
事件,並根據 URL 載入對應的內容
function handleInitialLoad() {
const path = window.location.pathname.substr(1);
if (path) {
navigateTo(path);
} else {
navigateTo('home');
}
}
window.addEventListener('load', handleInitialLoad);
後端則需要協助修改伺服器設定,就跟大多數的 SPA 一樣,我們只會有一個入口頁面,所以要將路由指向同一份 HTML 檔案。以 Nginx 為例子,設定可能如下:
location / {
try_files $uri $uri/ /index.html;
}
使用者輸入什麼 URL,伺服器都會根據設定返回 index.html
,再從前端處理路由。
這邊以我的部落格文章為例子,實作不用重新載入就能顯示文章內容。
一樣先處理 HTML 元素,連結是我部落格文章的實際 URL,我去爬文章內容,再將它渲染到 <div id="content"></div>
<nav>
<a href="cdn-tailwindcss-vscode-enable-tailwindcss-intellisense/">文章一</a>
<a href="introduction-singleton-design-pattern/">文章二</a>
<a href="quill-react-ant-design-and-upload-image/">文章三</a>
</nav>
<div id="content"></div>
我修改了 navigateTo()
函式,用 async/await
語法處理非同步操作,再用 fetch()
取得我的文章內容
async function navigateTo(page) {
showLoading();
try {
const res = await fetch(`https://muki.tw/${page}`);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const htmlContent = await res.text();
const extractedContent = extractContent(htmlContent);
history.pushState({ page, content: extractedContent }, page, `/${page}`);
updateContent(extractedContent);
} catch (error) {
console.error('Fetch error:', error);
updateContent('載入失敗,請稍後再試。');
} finally {
hideLoading();
}
}
res.text()
會顯示從整份 HTML 文件,但我只想要文章內容,所以用 extractContent()
來處理最後要顯示的 HTML
function extractContent(htmlString) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
// 使用屬性選擇器來選擇目標 div
const targetDiv = doc.querySelector('div[class*="min-h-[calc(100vh-70px)]"]');
if (targetDiv) {
return targetDiv.innerHTML;
} else {
// 如果找不到指定的 div,嘗試獲取 body 內容
const bodyContent = doc.body.innerHTML;
return bodyContent || '找不到指定的內容';
}
}
載入文章時,可以適時地增添 loading 效果,讓體驗更順暢
function showLoading() {
const loadingElement = document.createElement('div');
loadingElement.className = 'loading';
contentElement.appendChild(loadingElement);
}
function hideLoading() {
const loadingElement = document.querySelector('.loading');
if (loadingElement) {
loadingElement.remove();
}
}
以上雖是一個簡易的實作,但我們整合了 History API、非同步以及 loading 效果處理,讓大家了解 History API 可以有怎樣的運用,也許未來在使用現代框架時,可以更瞭解路由端的設計原理與實作方式。
我們能使用 History API 實現頁面切換,同時支援深度連結,還能保持瀏覽器歷史的完整性。但在使用上,也需要注意一些潛在問題:
以上就是 History API 的介紹,有任何問題都歡迎留言討論唷。